/**
 * Copyright 2009 The University of North Carolina at Chapel Hill
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * The original code was produced by Bing Zhu of DICE.
 */
package irodsfedora.storage.lowlevel;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import org.apache.commons.codec.binary.Hex;
import org.apache.log4j.Logger;

import edu.sdsc.grid.io.FileFactory;
import edu.sdsc.grid.io.FileMetaData;
import edu.sdsc.grid.io.GeneralFile;
import edu.sdsc.grid.io.MetaDataCondition;
import edu.sdsc.grid.io.MetaDataRecordList;
import edu.sdsc.grid.io.MetaDataSelect;
import edu.sdsc.grid.io.GeneralFile.Checksum;
import edu.sdsc.grid.io.irods.IRODSAccount;
import edu.sdsc.grid.io.irods.IRODSFile;
import edu.sdsc.grid.io.irods.IRODSFileInputStream;
import edu.sdsc.grid.io.irods.IRODSFileOutputStream;
import edu.sdsc.grid.io.irods.IRODSFileSystem;
import edu.sdsc.grid.io.irods.IRODSMetaDataSet;
import fedora.server.errors.LowlevelStorageException;

/**
 * This class is an adapter to bridge between the Fedora default lowlevel storage file system interface and the iRODS
 * Jargon API.  This adapter guarantees faithful transport for all write operations to iRODS via MD5 checksum
 * comparison.  Any failed checksum comparison results in delete of the file in question from iRODS and throwing a
 * LowlevelStorageException.  The class uses Java I/O buffered streams to prevent caller code from blocking when
 * possible.  In addition to the standard Fedora lowlevel storage operations, this class has a getMetadata operation,
 * which queries iRODS for a standard set of system metadata.
 *
 * @author Gregory Jansen
 *
 */
public class IrodsIFileSystem {

    @Override
    protected void finalize() throws Throwable {
	super.finalize();
	this.getFileSystem().close();
    }

    private static final int BUFFER_SIZE = 32768;
    /** Logger for this class. */
    private static final Logger LOG = Logger.getLogger(IrodsIFileSystem.class);

    private static final String stream2streamCopy(InputStream in, OutputStream out) throws IOException {
	LOG.debug("IrodsIFileSystem.stream2streamCopy() start");
	String result = null;
	byte[] buffer = new byte[BUFFER_SIZE];
	int bytesRead = 0;
	try {
	    MessageDigest messageDigest;
	    try {
		messageDigest = MessageDigest.getInstance("MD5");
	    } catch (NoSuchAlgorithmException e) {
		throw new IOException("Cannot compare checksums without MD5 algorithm.", e);
	    }
	    messageDigest.reset();
	    while ((bytesRead = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
		messageDigest.update(buffer, 0, bytesRead);
		// for transport failure test add the following line:
		// buffer[0] = '!';
		out.write(buffer, 0, bytesRead);
	    }
	    Hex hex = new Hex();
	    result = new String(hex.encode(messageDigest.digest()));
	} catch (IOException e) {
	    throw e;
	} finally {
	    out.flush();
	    out.close();
	}
	LOG.debug("IrodsIFileSystem.stream2streamCopy() end");
	return result;
    }

    IRODSAccount irodsAccount = null;
    IRODSFileSystem conn = null;

    int irodsReadBufferSize;

    int connectionsUsed = 0;
    int currentConnectionUsage = 0;

    public IrodsIFileSystem(IRODSAccount account, int readBufferSize) throws LowlevelStorageException {
	LOG.debug("IrodsIFileSystem.IrodsIFileSystem()");
	this.irodsAccount = account;
	this.irodsReadBufferSize = readBufferSize;
	try {
	    getFileSystem();
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}
    }

    protected IRODSFileSystem getFileSystem() throws IOException {
	if (conn != null && conn.isConnected()) {
	    currentConnectionUsage++;
	    return conn;
	}

	try {
	    connectionsUsed++;
	    conn = (IRODSFileSystem) FileFactory.newFileSystem(irodsAccount);
	    LOG.info("Created iRODS connection #" + connectionsUsed);
	    currentConnectionUsage = 1;
	} catch (Exception e) {
	    throw new IOException("connect_to_irods() failed." + e);
	}
	return conn;
    }

    public final void delete(File file) throws LowlevelStorageException {
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}
	LOG.debug("IrodsIFileSystem->delete(): " + file.getAbsolutePath());
	new IRODSFile(fileSystem, file.getAbsolutePath()).delete();
    }

    public boolean deleteDirectory(String directory) {
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new Error("could not obtain IRODS File System", e);
	}
	LOG.debug("IrodsIFileSystem->deleteDirectory():" + directory);
	boolean result = false;
	if (directory != null) {
	    IRODSFile file = new IRODSFile(fileSystem, directory);
	    if (file.exists() && file.isDirectory()) {
		// 1. delete content of directory:
		GeneralFile[] files = file.listFiles();
		result = true; // init result flag
		int count = files.length;
		for (int i = 0; i < count; i++) { // for each file:
		    GeneralFile f = files[i];
		    if (f.isFile()) {
			result = result && f.delete();
		    } else if (f.isDirectory()) {
			result = result && deleteDirectory(f.getAbsolutePath());
		    }
		}// next file
		file.delete(); // finally delete (empty) input directory
	    }// else: input directory does not exist or is not a directory
	}// else: no input value
	return result;
    }

    private String getMD5ChecksumFromIRODS(IRODSFile file) throws IOException {
	IRODSFileSystem fileSystem = getFileSystem();
	String irodschecksum = file.firstQueryResult(FileMetaData.FILE_CHECKSUM);
	if (irodschecksum == null || irodschecksum.trim().length() == 0) {
	    irodschecksum = file.checksum(Checksum.MD5);
	    LOG.info("IRODS CREATED A CHECKSUM: " + irodschecksum);
	} else {
	    LOG.info("IRODS ALREADY HAS A CHECKSUM: " + irodschecksum);
	}
	return irodschecksum;
    }

    public boolean isDirectory(File file) {
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new Error("could not obtain IRODS File System", e);
	}
	LOG.debug("IrodsIFileSystem.isDirectory():" + file.getAbsolutePath());
	return (new IRODSFile(fileSystem, file.getAbsolutePath())).isDirectory();
    }

    public String[] list(File directory) {
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new Error("could not obtain IRODS File System", e);
	}
	LOG.debug("IrodsIFileSystem->list():" + directory.getAbsolutePath());
	IRODSFile irodsDir = new IRODSFile(fileSystem, directory.getAbsolutePath());
	if (irodsDir.isDirectory()) {
	    return irodsDir.list();
	} else {
	    return null;
	}
    }

    public final InputStream read(File file) throws LowlevelStorageException {
	LOG.debug("IrodsIFileSystem->read(): " + file.getAbsolutePath() + " with buffer of " + irodsReadBufferSize);
	LOG.debug("THREAD:" + Thread.currentThread().getId());
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}

	InputStream result = null;
	IRODSFile source = new IRODSFile(fileSystem, file.getAbsolutePath());
	IRODSFileInputStream in = null;

	LOG.debug("checking if file.exists() before read");
	if (!source.exists()) {
	    throw new LowlevelStorageException(true, "file " + source + "doesn't exist for reading");
	}
	LOG.debug("checking if file.canRead() before read");
	if (!source.canRead()) {
	    throw new LowlevelStorageException(true, "file " + source + "not readable");
	}

	LOG.debug("IrodsIFileSystem->read(): source=" + source);
	try {
	    in = new IRODSFileInputStream(source);
	    if (LOG.isDebugEnabled()) {
		LOG.debug("The IRODSFileInputStream.available() method says:" + in.available());
	    }
	} catch (IOException eCaughtOpenFile) {
	    throw new LowlevelStorageException(true, "file " + source + "couldn't be opened for reading",
			    eCaughtOpenFile);
	}
	result = new BufferedInputStream(in, irodsReadBufferSize);
	return result;
    }

    public void rewrite(File file, InputStream content) throws LowlevelStorageException {
	LOG.debug("IrodsIFileSystem.rewrite(): " + file.getAbsolutePath());
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}

	long now = new Date().getTime();
	IRODSFile destination = new IRODSFile(fileSystem, file.getAbsolutePath());
	IRODSFile temp = new IRODSFile(fileSystem, destination.getAbsolutePath() + ".temp." + now);
	IRODSFile old = new IRODSFile(fileSystem, destination.getAbsolutePath() + ".old." + now);
	IRODSFile trueLocation = new IRODSFile(fileSystem, file.getAbsolutePath());

	if (!destination.exists()) {
	    throw new LowlevelStorageException(true, "File to rewrite does not exist! (" + destination + ")");
	}

	try {
	    IRODSFileOutputStream out = new IRODSFileOutputStream(temp);

	    String localchecksum = stream2streamCopy(content, out);

	    // get IRODS checksum
	    String irodschecksum = this.getMD5ChecksumFromIRODS(temp);
	    if (!localchecksum.equals(irodschecksum)) {
		LOG.debug("local and iRODS checksums DO NOT MATCH");
		if (!temp.delete()) {
		    throw new LowlevelStorageException(true, temp.getAbsolutePath()
				    + " did not match local checksum and could not be deleted");
		} else {
		    throw new LowlevelStorageException(true, temp.getAbsolutePath()
				    + " did not match local checksum, file was deleted");
		}
	    }
	    if (!destination.renameTo(old)) {
		throw new LowlevelStorageException(true, destination.getAbsolutePath() + " could not be renamed to "
				+ old.getAbsolutePath());
	    }
	    if (!temp.renameTo(trueLocation)) {
		throw new LowlevelStorageException(true, temp.getAbsolutePath() + " could not be renamed to "
				+ trueLocation.getAbsolutePath());
	    }
	    if (!old.delete()) {
		throw new LowlevelStorageException(true, old.getAbsolutePath() + " could not be deleted");
	    }
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "IRODSFedoraFileSystem.rewrite(): [" + destination + "]", e);
	}
    }

    public final void write(File file, InputStream content) throws LowlevelStorageException {
	LOG.debug("IrodsIFileSystem.write(): " + file.getAbsolutePath());
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}

	IRODSFile destination = new IRODSFile(fileSystem, file.getAbsolutePath());
	try {
	    destination.createNewFile();
	    IRODSFileOutputStream out = new IRODSFileOutputStream(destination);

	    String localchecksum = stream2streamCopy(content, out);

	    // get IRODS checksum
	    String irodschecksum = this.getMD5ChecksumFromIRODS(destination);
	    if (!localchecksum.equals(irodschecksum)) {
		LOG.debug("local and iRODS checksums DO NOT MATCH");
		if (!destination.delete()) {
		    throw new LowlevelStorageException(true, destination.getAbsolutePath()
				    + " did not match local checksum and could not be deleted");
		} else {
		    throw new LowlevelStorageException(true, destination.getAbsolutePath()
				    + " did not match local checksum, file was deleted");
		}
	    }
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "IRODSFedoraFileSystem.write(): couldn't make directories for ["
			    + destination + "]", e);
	}
    }

    public MetaDataRecordList[] getMetadata(String path) throws LowlevelStorageException {
	LOG.debug("IrodsIFileSystem.getMetadata(): " + path);
	MetaDataRecordList[] result = null;
	String[] fields = { IRODSMetaDataSet.SIZE,
		IRODSMetaDataSet.MODIFICATION_DATE,
		IRODSMetaDataSet.CREATION_DATE,
		IRODSMetaDataSet.OWNER,
		IRODSMetaDataSet.FILE_CHECKSUM,
		IRODSMetaDataSet.FILE_VERSION,
		IRODSMetaDataSet.FILE_NAME,
		IRODSMetaDataSet.DIRECTORY_NAME,
		IRODSMetaDataSet.FILE_REPLICA_NUM,
		IRODSMetaDataSet.FILE_REPLICA_STATUS,
		IRODSMetaDataSet.RESOURCE_NAME,
		IRODSMetaDataSet.RESOURCE_LOCATION,
		IRODSMetaDataSet.RESOURCE_CLASS,
		IRODSMetaDataSet.RESOURCE_COMMENTS,
		IRODSMetaDataSet.RESOURCE_INFO,
		IRODSMetaDataSet.RESOURCE_TYPE,
		IRODSMetaDataSet.RESOURCE_VAULT_PATH,
		IRODSMetaDataSet.RESOURCE_ZONE
		};

	MetaDataSelect[] metadataSelects = IRODSMetaDataSet.newSelection( fields );
	IRODSFileSystem fileSystem;
	try {
	    fileSystem = getFileSystem();
	    IRODSFile file = new IRODSFile(fileSystem, path);
	    LOG.debug("about to call file.query()");
	    MetaDataCondition[] conditions = new MetaDataCondition[2];
	    conditions[0] =
	        IRODSMetaDataSet.newCondition( IRODSMetaDataSet.DIRECTORY_NAME, MetaDataCondition.EQUAL, file.getParent() );
	    conditions[1] =
	        IRODSMetaDataSet.newCondition( IRODSMetaDataSet.FILE_NAME, MetaDataCondition.EQUAL, file.getName() );
	    result = fileSystem.query(conditions, metadataSelects);
	} catch (IOException e) {
	    throw new LowlevelStorageException(true, "could not obtain IRODS File System", e);
	}
	return result;
    }
}
